package com.schoolcloud.files.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSON;
import com.schoolcloud.common.exception.FileUploadException;
import com.schoolcloud.common.model.PageDTO;
import com.schoolcloud.common.model.R;
import com.schoolcloud.common.util.UserContext;
import com.schoolcloud.files.config.FileConfig;
import com.schoolcloud.files.dto.ChunkCheckDTO;
import com.schoolcloud.files.dto.ChunkMergeDTO;
import com.schoolcloud.files.dto.FileInfoDTO;
import com.schoolcloud.files.entity.FileAccessLog;
import com.schoolcloud.files.entity.FileMain;
import com.schoolcloud.files.entity.FileUploadScene;
import com.schoolcloud.files.query.FileMainQuery;
import com.schoolcloud.files.service.*;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.implementation.bytecode.assign.TypeCasting;
import org.apache.tomcat.util.http.fileupload.FileUtils;
import org.bytedeco.opencv.opencv_videostab.IFrameSource;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
@RequiredArgsConstructor
@Slf4j
public class MinioFileServiceImpl implements FileService {

    private final MinioClient minioClient;
    private final FileMainService fileMainService;
    private final FileAccessLogService fileAccessLogService;
    private final StorageBucketService storageBucketService;
    private final FileUploadSceneService fileUploadSceneService;

    @Override
    public FileInfoDTO uploadFile(MultipartFile file, String bucket, String baseUrl, String sceneCode)  {
        FileUploadScene byCode = fileUploadSceneService.getBySceneCode(sceneCode);
        System.out.println("进入方法");
         bucket = byCode.getBucketName();
        String objectName = null;
        String md5 = null;
        try (InputStream rawStream = file.getInputStream()) {
            BufferedInputStream bufferedStream = new BufferedInputStream(rawStream);
            // 1. 设置标记以便后续重置
            bufferedStream.mark(Integer.MAX_VALUE);
            // 2. 计算 MD5（会读取整个流）
            md5 = DigestUtil.md5Hex(bufferedStream);
            // 3. 重置流到标记位置(mark)
            bufferedStream.reset();
            // 4. 生成存储路径
            objectName = generatePath(byCode.getPathTemplate(), md5) + file.getOriginalFilename();
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucket)
                            .object(objectName)
                            .contentType(file.getContentType())
                            .stream(bufferedStream, file.getSize(), -1) // 使用实际文件大小
                            .build());
            FileMain fileMain = new FileMain();
            fileMain.setFileType(file.getContentType());
            fileMain.setFileSize(file.getSize());
            fileMain.setSceneCode(sceneCode);
            fileMain.setUploaderId(UserContext.getUser());
            fileMain.setUploaderType(UserContext.getUserType());
            fileMain.setOriginalName(file.getOriginalFilename());
            fileMain.setStorageBucket(bucket);
            fileMain.setStoragePath(FileConfig.PROFILE + "/" + bucket+"/"+objectName);
            fileMain.setMd5(DigestUtils.md5DigestAsHex(file.getInputStream()));
            fileMainService.save(fileMain);
            FileInfoDTO fileInfoDTO = new FileInfoDTO();
            BeanUtils.copyProperties(fileMain, fileInfoDTO);
            return fileInfoDTO;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public FileInfoDTO getFileInfo(String filePath) {
        FileMain fileMain = fileMainService.getByFilePath(filePath);
        FileInfoDTO fileInfoDTO = new FileInfoDTO();
        BeanUtils.copyProperties(fileMain, fileInfoDTO);
        return fileInfoDTO;
    }

    @Override
    public boolean checkFileExist(String md5) {
        FileMain fileMain = fileMainService.getByMd5(md5);
        if (fileMain == null) {
            return false;
        }
        StatObjectArgs build = StatObjectArgs.builder().bucket(fileMain.getStorageBucket()).object(fileMain.getStoragePath()).build();
        try {
            minioClient.statObject(build);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean checkChunkExist(ChunkCheckDTO dto) {
        int index = dto.getIndex();
        String md5 = dto.getMd5();
        FileUploadScene scene = fileUploadSceneService.getBySceneCode(dto.getSceneCode());
        if (scene==null){
            throw new FileUploadException("场景编码错误！");
        }
        String bucketName =scene.getBucketName();
        String chunkPath = generatePath(scene.getPathTemplate(), md5);
        String objectName = chunkPath + index;
        // 使用statObject方法检查对象是否存在
        StatObjectArgs args = StatObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build();
        try {
            minioClient.statObject(args);
            return true;
        } catch (ErrorResponseException e) {
            if ("NoSuchKey".equals(e.errorResponse().code())) {
                return false;
            }
            // 其他错误继续抛出或处理
            log.error("检查分片存在时发生错误: {}", e.getMessage(), e);
        } catch (Exception e) {
            // 处理其他异常
            log.error("检查分片存在时发生未知错误: {}", e.getMessage(), e);
        }
        return false;
    }

    @Override
    public boolean uplaodChunk(String sceneCode, MultipartFile file, String videoMD5, int chunk) {
        FileUploadScene scene = fileUploadSceneService.getBySceneCode(sceneCode);
        if (scene==null){
            log.error("场景编码{}错误！", sceneCode);
            throw new FileUploadException("场景编码错误！");
        }
        String pathTemplate = scene.getPathTemplate();
        String path = generatePath(pathTemplate, videoMD5);
        System.out.println("chunkPath:"+path+"模板"+pathTemplate);
//        拼接保存路径
//        String chunkPath = getFilePath(videoMD5) + chunk;
        String chunkPath = path + chunk;
        try (InputStream inputStream = file.getInputStream()) {
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(scene.getBucketName())
                    .object(chunkPath)
                    .stream(inputStream, file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build());
            return true;
        } catch (Exception e) {
            log.error("上传分片时发生错误: {}", e.getMessage(), e);
        }
        return false;
    }

    @Override
    public R<?> mergeChunk(ChunkMergeDTO dto) {
        String md5 = dto.getMd5();
        String fileName = dto.getFileName();
        int chunkTotal = dto.getChunkTotal();
        FileUploadScene scene = fileUploadSceneService.getBySceneCode(dto.getMergeScene());
        FileUploadScene chunkScene = fileUploadSceneService.getBySceneCode(dto.getChunkScene());
        String bucketName = scene.getBucketName();
        String filePath = generatePath(scene.getPathTemplate(), md5) + fileName;
        String chunkPath = generatePath(chunkScene.getPathTemplate(), md5);
        List<ComposeSource> collect = Stream.iterate(0, i -> ++i).limit(chunkTotal).map(i -> ComposeSource.builder().bucket(bucketName).object(chunkPath + i).build()).collect(Collectors.toList());
        try {
            minioClient.composeObject(ComposeObjectArgs.builder().bucket(bucketName).object(filePath).sources(collect).build());
//            校验
            try (InputStream object = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucketName)
                            .object(filePath)
                            .build())) {
                String s = DigestUtils.md5DigestAsHex(object);
                if (md5.equals(s)) {
//                    获取元信息
                    StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder()
                            .bucket(bucketName)
                            .object(filePath)
                            .build());
//                    保存文件信息
                    FileMain fileMain = new FileMain();
                    fileMain.setSceneCode(scene.getSceneCode());
                    fileMain.setFileType(stat.contentType());
                    fileMain.setOriginalName(fileName);
                    fileMain.setStorageBucket(scene.getBucketName());
                    fileMain.setStoragePath(filePath);
                    fileMain.setFileSize(stat.size());
                    fileMain.setMd5(md5);
//                    fileMain.setStatus(FileStatusEnum.WAITING.getCode());
                    fileMain.setVisitPath(FileConfig.PROFILE + "/" +bucketName+"/"+ filePath);
                    fileMain.setUploaderId(UserContext.getUser());
                    fileMain.setUploaderType(UserContext.getUserType());
                    fileMain.setUploadTime(LocalDateTime.now());
//                    fileMain.setExpireTime(LocalDateTime.now().plusDays(scene.getExpireTime().toInstant(ZoneOffset.UTC).toEpochMilli()));//后期考虑实现
                    fileMainService.save(fileMain);
//                    删除分块文件
                    clearChunkFile(bucketName, getFilePath(md5), chunkTotal);
                    return R.ok(new FileInfoDTO(fileMain));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("合并文件失败：{},桶：{}，文件：{}", e.getMessage(), bucketName, filePath);
        }
        return R.err();
    }

    @Override
    public FileInfoDTO commonUpload(MultipartFile file, String sceneCode) {
        // 验证场景和文件类型
        FileUploadScene byCode = fileUploadSceneService.getByCode(sceneCode);
        if (byCode == null) {
            log.error("场景{}不存在!", sceneCode);
            throw new FileUploadException("场景不存在!");
        }
        if (!isValidFileType(file, byCode.getAllowFileTypes())) {
            log.error("场景：{},文件类型不合法!",sceneCode);
            throw new FileUploadException("文件类型不合法!");
        }
        String bucket = byCode.getBucketName();
        String objectName = null;
        String md5 = null;
        try (InputStream rawStream = file.getInputStream()) {
            // 包装成 BufferedInputStream 以支持 mark/reset
            BufferedInputStream bufferedStream = new BufferedInputStream(rawStream);
            // 1. 设置标记以便后续重置
            bufferedStream.mark(Integer.MAX_VALUE);
            // 2. 计算 MD5（会读取整个流）
            md5 = DigestUtil.md5Hex(bufferedStream);
            if (checkFileExist(md5)) {
                return new FileInfoDTO(fileMainService.getByMd5(md5));
            }
            // 3. 重置流到标记位置(mark)
            bufferedStream.reset();
            // 4. 生成存储路径
            objectName = generatePath(byCode.getPathTemplate(), md5) + file.getOriginalFilename();
            // 5. 上传到 MinIO（使用重置后的流）
            System.out.println("信息"+bucket+"路径"+objectName);
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .stream(bufferedStream, file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build());
            // 6. 文件信息入库
            FileMain fileMain = new FileMain();
            fileMain.setSceneCode(byCode.getSceneCode());
            fileMain.setOriginalName(file.getOriginalFilename());
            fileMain.setFileType(file.getContentType());
            fileMain.setStorageBucket(bucket);
            fileMain.setStoragePath(objectName);
            fileMain.setVisitPath(FileConfig.PROFILE + "/" + bucket+"/"+objectName);
            fileMain.setFileSize(file.getSize());
            fileMain.setMd5(md5);
            fileMainService.save(fileMain);
            return new FileInfoDTO(fileMain);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("文件上传失败! scene={}, file={}m", sceneCode, file.getOriginalFilename());
            throw new FileUploadException("上传失败: " + e.getMessage());
        }
    }

    private String getFilePath(String videoMD5) {
        return videoMD5.charAt(0) + "/" + videoMD5.charAt(1) + "/" + videoMD5 + "/chunks/";
    }

    private void clearChunkFile(String sharePublicBucket, String chunkFolderPth, int chunkTotal) {
        List<DeleteObject> collect = Stream.iterate(0, i -> ++i).limit(chunkTotal).map(i -> new DeleteObject(chunkFolderPth + i)).collect(Collectors.toList());
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(sharePublicBucket).objects(collect).build());
        results.forEach(result -> {
            try {
                DeleteError error = result.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    private boolean isValidFileType(MultipartFile file, String allowedExtensions) {
        if (StrUtil.isBlank(allowedExtensions)){
            return true;
        }
        if (file == null || file.isEmpty()) {
            return false;
        }
        // 允许的扩展名集合
        Set<String> allowedExtSet = Arrays.stream(allowedExtensions.split(","))
                .map(String::trim)
                .map(String::toLowerCase)
                .collect(Collectors.toSet());
        // 获取文件实际扩展名（从原始文件名提取）
        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null) {
            return false;
        }
        String fileExtension = StringUtils.getFilenameExtension(originalFilename);
        if (fileExtension == null) {
            return false;
        }
        // 检查扩展名是否允许
        return allowedExtSet.contains(fileExtension.toLowerCase());
    }

    private String generatePath(String pathTemplate, String md5) {
        // 处理 {date} 占位符
        String result = pathTemplate.replace("{date}",
                YearMonth.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
//        处理{uerId}
        result = result.replace("{userId}",UserContext.getUser());
        // 处理 {md5[X]} 占位符，其中 X 是索引位置
        Pattern pattern = Pattern.compile("\\{md5\\[(\\d+)\\]\\}");
        Matcher matcher = pattern.matcher(result);
        while (matcher.find()) {
            int index = Integer.parseInt(matcher.group(1));
            if (index >= 0 && index < md5.length()) {
                String replacement = String.valueOf(md5.charAt(index));
                result = result.replace(matcher.group(0), replacement);
            } else {
                throw new IllegalArgumentException("MD5 索引超出范围: " + index);
            }
        }
        // 处理 {md5} 完整替换
        result = result.replace("{md5}", md5);
        return result;
    }

}
